Skip to content

Conversation

06b
Copy link
Contributor

@06b 06b commented Sep 8, 2025

Description

This relates to the request for High Contrast Support #21515.

When forced-colors is active (such as High Contrast Mode in Windows), in Vuetify 3.9.6 it would render VListItem that is a link with the expected system color for the hyperlink.

Screenshot 2025-09-08 163115

However, due to #21958 in Vuetify 3.9.7 the VListItem renders the incorrect expected system colors for a VListItem that is a link or has no action assigned to it as the button system color.

For example, Content Filtering has no action assigned & Password is a link in the screenshots shown.
Screenshot 2025-09-08 163154

This pull request corrects this mistake when in forced-colors mode.
Screenshot 2025-09-08 164321

Markup:

<template>

  <v-card class="mx-auto" max-width="400">
    <v-toolbar color="purple">
      <v-btn icon="mdi-menu" />
      <v-toolbar-title>Settings</v-toolbar-title>
      <v-btn icon="mdi-magnify" />
    </v-toolbar>

    <v-list lines="three">
      <v-list-subheader>User Controls</v-list-subheader>
      <v-list-item
        v-for="item in userControls"
        :key="item.value"
        :href="item.href"
        :subtitle="item.subtitle"
        :title="item.title"
      >
        <template #prepend>
          <v-avatar>
            <v-icon :icon="item.icon" />
          </v-avatar>
        </template>
      </v-list-item>
    </v-list>

    <v-divider />

    <v-list
      v-model:selected="settingsSelection"
      lines="three"
      select-strategy="leaf"
    >
      <v-list-subheader>General</v-list-subheader>
      <v-list-item
        v-for="item in settingsItems"
        :key="item.value"
        :subtitle="item.subtitle"
        :title="item.title"
        :value="item.value"
      >
        <template #prepend="{ isSelected, select }">
          <v-list-item-action start>
            <v-checkbox-btn
              :model-value="isSelected"
              @update:model-value="select"
            />
          </v-list-item-action>
        </template>
      </v-list-item>
    </v-list>
  </v-card>

</template>

<script>
  export default {
    data: () => ({
      userControls: [
        { title: 'Content filtering', subtitle: 'Set the content filtering level to restrict appts that can be downloaded', icon: 'mdi-filter' },
        { title: 'Password', subtitle: 'Require password for purchase or use password to restrict purchase', icon: 'mdi-form-textbox-password', href: 'https://example.com' },
      ],

      settingsItems: [
        { value: 'notifications', title: 'Notifications', subtitle: 'Notify me about updates to apps or games that I downloaded' },
        { value: 'sound', title: 'Sound', subtitle: 'Auto-update apps at any time. Data charges may apply' },
        { value: 'widgets', title: 'Auto-add widgets', subtitle: 'Automatically add home screen widgets when downloads complete' },
      ],

      settingsSelection: [],
    }),
  }
</script>

@06b 06b force-pushed the fix/vlist-forced-colors branch from c36fb92 to 3564664 Compare October 2, 2025 13:20
@06b
Copy link
Contributor Author

06b commented Oct 3, 2025

@J-Sek do you mind giving this a review?

@J-Sek J-Sek assigned 06b Oct 3, 2025
@J-Sek J-Sek added a11y Accessibility issue C: VListItem labels Oct 3, 2025
@06b
Copy link
Contributor Author

06b commented Oct 7, 2025

Updated demo below

<template>
  <v-app>
    <v-container>
      <v-row>
        <v-col>
          <h1>Forced-Colors VList Playground Test #22032</h1>
          <ul>
            <li>(Native Links)
              <ul>
                <li>
                  <a href="https://example.com">Example</a>
                </li>
                <li>
                  <a aria-current="page" href="/">Example with aria-current</a>
                </li>
              </ul>
            </li>
          </ul>
        </v-col>
        <v-col>
          <fieldset class="pa-2">
            <legend>Is Life Unfair? (Native Checkbox)</legend>
            <div>
              <input id="yes" type="checkbox" checked>
              <label for="yes">Yes &mdash; checked</label>
            </div>
            <div>
              <input id="no" type="checkbox">
              <label for="no">No &mdash; unchecked</label>
            </div>
            <div>
              <input id="maybe" type="checkbox" indeterminate>
              <label for="maybe">Maybe &mdash; indeterminate</label>
            </div>
            <div>
              <input id="dunno" type="checkbox" checked indeterminate>
              <label for="dunno">I don't know &mdash; checked indeterminate</label>
            </div>
            <div>
              <input id="repeat" type="checkbox" disabled>
              <label for="repeat">Can you repeat the question? &mdash; disabled</label>
            </div>
            <div>
              <input id="yas" type="checkbox" checked disabled>
              <label for="yas">Yas &mdash; checked disabled</label>
            </div>
            <div>
              <input id="no!" type="checkbox" disabled indeterminate>
              <label for="no!">NO! &mdash; disabled indeterminate</label>
            </div>
          </fieldset>
        </v-col>
      </v-row>
      <v-row v-for="(variant, i) in variants" :key="i">
        <v-col>
          <h2>Action and item groups</h2>
          <v-card class="mx-auto" max-width="400">
            <v-toolbar color="purple">
              <v-btn icon="mdi-menu" />
              <v-toolbar-title>Settings ({{ variant }})</v-toolbar-title>
              <v-btn icon="mdi-magnify" />
            </v-toolbar>

            <v-list :variant="variant" active-class="text-blue" lines="three">
              <v-list-subheader>User Controls</v-list-subheader>
              <v-list-item
                v-for="item in userControls"
                :key="item.value"
                :href="item.href"
                :subtitle="item.subtitle"
                :title="item.title"
                :to="item.to"
              >
                <template #prepend>
                  <v-avatar>
                    <v-icon :icon="item.icon" />
                  </v-avatar>
                </template>
              </v-list-item>
            </v-list>

            <v-divider />

            <v-list
              v-model:selected="settingsSelection"
              :variant="variant"
              active-class="text-blue"
              lines="three"
              select-strategy="leaf"
            >
              <v-list-subheader>General</v-list-subheader>
              <v-list-item
                v-for="item in settingsItems"
                :key="item.value"
                :disabled="item.disabled"
                :subtitle="item.subtitle"
                :title="item.title"
                :value="item.value"
              >
                <template #prepend="{ isSelected, select }">
                  <v-list-item-action start>
                    <v-checkbox-btn
                      :model-value="isSelected"
                      @update:model-value="select"
                    />
                  </v-list-item-action>
                </template>
              </v-list-item>
            </v-list>
          </v-card>
        </v-col>
        <v-col>
          <h2>Action with text</h2>
          <v-card class="mx-auto" max-width="500">
            <v-toolbar color="pink">
              <v-btn icon="mdi-menu" />

              <v-toolbar-title>Inbox ({{ variant }})</v-toolbar-title>

              <v-btn icon="mdi-magnify" />

              <v-btn icon="mdi-checkbox-marked-circle" />
            </v-toolbar>

            <v-list v-model:selected="selected" :variant="variant" select-strategy="leaf">
              <v-list-item
                v-for="item in itemsForActionWithTextDemo"
                :key="item.id"
                :disabled="item.disabled"
                :href="item.href"
                :to="item.to"
                :value="item.id"
                active-class="text-pink"
                class="py-3"
              >
                <v-list-item-title>{{ item.title }}</v-list-item-title>

                <v-list-item-subtitle class="mb-1 text-high-emphasis opacity-100">{{ item.headline }}</v-list-item-subtitle>

                <v-list-item-subtitle class="text-high-emphasis">{{ item.subtitle }}</v-list-item-subtitle>

                <template #append="{ isSelected }">
                  <v-list-item-action class="flex-column align-end">
                    <small class="mb-4 text-high-emphasis opacity-60">{{ item.action }}</small>

                    <v-spacer />

                    <v-icon v-if="isSelected" color="yellow-darken-3">mdi-star</v-icon>

                    <v-icon v-else class="opacity-30">mdi-star-outline</v-icon>
                  </v-list-item-action>
                </template>
              </v-list-item>
            </v-list>
          </v-card>
        </v-col>
        <v-col>
          <h2>Sub Group List</h2>
          <v-card
            class="mx-auto"
            width="300"
          >
            <v-list v-model:opened="open" :variant="variant">
              <v-list-item prepend-icon="mdi-home" title="Home" />

              <v-list-group value="Users">
                <template #activator="{ props }">
                  <v-list-item
                    v-bind="props"
                    prepend-icon="mdi-account-circle"
                    title="Users"
                  />
                </template>

                <v-list-group value="Admin">
                  <template #activator="{ props }">
                    <v-list-item
                      v-bind="props"
                      title="Admin"
                    />
                  </template>

                  <v-list-item
                    v-for="([title, icon], index) in admins"
                    :key="index"
                    :prepend-icon="icon"
                    :title="title"
                    :value="title"
                  />
                </v-list-group>

                <v-list-group value="Actions">
                  <template #activator="{ props }">
                    <v-list-item
                      v-bind="props"
                      title="Actions"
                    />
                  </template>

                  <v-list-item
                    v-for="([title, icon], index) in cruds"
                    :key="index"
                    :prepend-icon="icon"
                    :title="title"
                    :value="title"
                  />
                </v-list-group>
              </v-list-group>
            </v-list>
          </v-card>

        </v-col>
      </v-row>
    </v-container>
  </v-app>
</template>

<script>
  export default {
    name: 'Playground',
    setup () {
      return {
        //
      }
    },
    data: () => ({
      variants: ['elevated', 'flat', 'tonal', 'outlined', 'text', 'plain'],
      open: ['Users'],
      admins: [
        ['Management', 'mdi-account-multiple-outline'],
        ['Settings', 'mdi-cog-outline'],
      ],
      cruds: [
        ['Create', 'mdi-plus-outline'],
        ['Read', 'mdi-file-outline'],
        ['Update', 'mdi-update'],
        ['Delete', 'mdi-delete'],
      ],
      userControls: [
        { title: 'Content filtering — No Action', subtitle: 'Set the content filtering level to restrict appts that can be downloaded', icon: 'mdi-filter' },
        { title: 'Password — Link (:href)', subtitle: 'Require password for purchase or use password to restrict purchase', icon: 'mdi-form-textbox-password', href: 'https://example.com' },
        { title: 'Two-Factor Authentication — Link (:to)', subtitle: 'Enable an extra layer of protection against unauthorized access with two-factor authentication', icon: 'mdi-two-factor-authentication', to: 'https://example.com' },
        { title: 'Location Proximity — Link (:to) & Active', subtitle: 'Restrict actions when your device is away from trusted and familiar locations', icon: 'mdi-selection-marker', to: '/' },
      ],

      settingsItems: [
        { value: 'notifications', title: 'Notifications', subtitle: 'Notify me about updates to apps or games that I downloaded' },
        { value: 'sound', title: 'Sound — Disabled & Selected', subtitle: 'Auto-update apps at any time. Data charges may apply', disabled: true },
        { value: 'widgets', title: 'Auto-add widgets — Disabled', subtitle: 'Automatically add home screen widgets when downloads complete', disabled: true },
      ],

      settingsSelection: ['sound'],
      items: [
        { text: 'Real-Time', icon: 'mdi-clock' },
        { text: 'Audience', icon: 'mdi-account' },
        { text: 'Conversions', icon: 'mdi-flag' },
      ],
      itemsForActionWithTextDemo: [
        { id: 1, action: '15 min', headline: 'Brunch this weekend?', subtitle: `I'll be in your neighborhood doing errands this weekend. Do you want to hang out?`, title: 'Ali Connors' },
        { id: 2, action: '2 hr', headline: 'Summer BBQ', subtitle: `Wish I could come, but I'm out of town this weekend.`, title: 'me, Scrott, Jennifer — Selected' },
        { id: 3, action: '6 hr', headline: 'Oui oui', subtitle: 'Do you have Paris recommendations? Have you ever been?', title: 'Sandra Adams — Link (:href)', href: 'https://example.com' },
        { id: 4, action: '12 hr', headline: 'Birthday gift', subtitle: 'Have any ideas about what we should get Heidi for her birthday?', title: 'Trevor Hansen — Link(:to)', to: 'https://example.com' },
        { id: 5, action: '18hr', headline: 'Recipe to try', subtitle: 'We should eat this: Grate, Squash, Corn, and tomatillo Tacos.', title: 'Britta Holt — Link(:to) & Active', to: '/' },
        { id: 6, action: '22hr', headline: 'Lunch?', subtitle: 'Food? I have no idea what I am doing...', title: 'Ada (Draft) — Disabled & Selected', disabled: true },
      ],
      selected: [2, 6],
    }),
  }
</script>

Comment on lines 350 to 364
&--variant-text.v-list-item--active:not(&--disabled),
&--variant-plain.v-list-item--active:not(&--disabled)
[class*="v-list-item-"],
[class*="v-list-item-"] > *,
.v-icon
color: highlight !important

&--active:not(&--variant-text, &--variant-plain)
background-color: highlight !important
color: highlighttext !important
&--active:not(&--variant-text, &--variant-plain):not(&--disabled)
background: highlight !important

.v-list-item-title,
.v-list-item-subtitle
[class*="v-list-item-"],
[class*="v-list-item-"] > *,
.v-icon
opacity: 1
color: highlighttext !important
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it could be refactored a bit

Suggested change
&--variant-text.v-list-item--active:not(&--disabled),
&--variant-plain.v-list-item--active:not(&--disabled)
[class*="v-list-item-"],
[class*="v-list-item-"] > *,
.v-icon
color: highlight !important
&--active:not(&--variant-text, &--variant-plain)
background-color: highlight !important
color: highlighttext !important
&--active:not(&--variant-text, &--variant-plain):not(&--disabled)
background: highlight !important
.v-list-item-title,
.v-list-item-subtitle
[class*="v-list-item-"],
[class*="v-list-item-"] > *,
.v-icon
opacity: 1
color: highlighttext !important
&--active:not(&--disabled)
[class*="v-list-item-"],
[class*="v-list-item-"] > *,
.v-icon
color: highlight !important
&--active:not(&--variant-text, &--variant-plain):not(&--disabled)
background: highlight !important
[class*="v-list-item-"],
[class*="v-list-item-"] > *,
.v-icon
opacity: 1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a11y Accessibility issue C: VListItem
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants